Pro ASP.NET Core MVC2(第7版)翻译

第21章:视图

作者:Adam Freeman 翻译:陈广 日期:2018-10-5


在第17章中,您看到了 action 方法如何返回ViewResult对象,它告诉 MVC 渲染视图并向客户端返回 HTML 响应。

在整本书中,您已经看到许多示例中都使用了视图,因此您大致知道它们的作用,但我在本章中详细介绍了这些细节。

我首先向您展示 MVC 如何使用视图引擎处理ViewResult对象,包括演示如何创建自定义视图引擎。我还描述了如何有效地使用内置 Razor 视图引擎的技术,包括使用分部视图和布局 sections,它们是有效开发 MVC 的基本主题。表21-1为视图来历。

表 21-1:视图来历

问题 回答
它们是什么? 视图是 MVC 模式的一部分,用于向用户显示内容。在 ASP.NET Core MVC 应用程序中,视图是包含 HTML 元素和 C# 代码的文件,它被处理以生成响应。
它们有何用途? 视图允许将数据表示与处理请求的逻辑分离。视图还允许在整个应用程序中应用相同的表示,因为许多控制器可以使用相同的视图。
如何使用它们 大多数 MVC 应用程序使用 Razor 视图引擎,它使得混合 HTML 和 C# 内容变得很容易。如第17章所述,视图是通过 action 方法返回的ViewResult对象来选择的。
是否有任何缺陷或限制? 要习惯 Razor 以及它的 HTML 和 C# 混合使用可能需要一段时间。本章我将解释 Razor 是如何工作的,这有助于理解它的一些操作。
有没有其他选择? 有许多第三方视图引擎可用于 MVC,但它们的使用是受限的。

表21-2为本章摘要

表 21-2:本章摘要

问题 解决方案 清单
创建自定义视图引擎 实现IViewEngineIView接口 3-6
轻松创建混合 HTML 和 C# 代码的响应 使用 Razor 视图引擎 7-11
定义要在布局中使用的内容区域 使用 Razor sections 12-18
创建可重用的标记片段 使用分部视图 19-22
向视图添加 JSON 内容 使用@Json.Serialze表达式 23-25
更改 Razor 搜索视图的位置 创建视图位置扩展程序 26-30

准备示例项目

本章我使用【ASP.NET Core Web 应用程序(.NET Core)】模板创建了一个名为 Views 的新的空项目。清单21-1显示了我对Startup类所做的更改,以启用 MVC 框架和开发所需的其他中间件。

清单 21-1:Startup.cs 文件的内容

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace Views
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

我创建了 Controllers 文件夹,并添加了一个名为 HomeController.cs 的文件,使用它定义了清单21-2所示的控制器。

清单 21-2:Controllers 文件夹下的 HomeController.cs 文件的内容

using System;
using Microsoft.AspNetCore.Mvc;

namespace Views.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index()
        {
            ViewBag.Message = "Hello, World";
            ViewBag.Time = DateTime.Now.ToString("HH:mm:ss");
            return View("DebugData");
        }

        public ViewResult List() => View();
    }
}

创建自定义视图引擎

我将深入核心,并创建一个自定义视图引擎。对于大多数项目,您不需要这样做,因为 MVC 包括 Razor 视图引擎,我在第5章中描述了它的语法,到目前为止,我在本书中的所有示例中都使用了它(不久将继续使用)。

创建自定义视图引擎的价值在于了解幕后发生的事情,并扩展对 MVC 工作方式的了解,包括了解视图引擎在将ViewResult转换为对客户端的响应方面有多大的自由度。

视图引擎是实现在Microsoft.AspNetCore.Mvc.ViewEngines命名空间中定义的IViewEngine接口的类。下面是IViewEngine接口的定义:

namespace Microsoft.AspNetCore.Mvc.ViewEngines {

    public interface IViewEngine {

    ViewEngineResult GetView(string executingFilePath, string viewPath,
        bool isMainPage);

    ViewEngineResult FindView(ActionContext context, string viewName,
        bool isMainPage);
    }
}

视图引擎的角色是将对视图的请求转换为ViewEngineResult对象。当 MVC 需要一个视图时,它首先调用GetView方法,这使视图引擎有机会使用它的名称提供视图。

如果GetView方法无法提供视图,则会调用FindView方法以使视图引擎有机会使用ActionContext对象查找视图,它提供有关创建ViewResult对象的 action 方法的信息。

视图引擎的工作是为 MVC 提供可以用于生成响应的ViewEngineResult对象。ViewEngineResult类不能直接实例化,而是提供用于创建实例的静态方法,如表21-3所述。

表 21-3:ViewEngineResult 类的静态方法

名称 描述
Found(name, view) 调用此方法为 MVC 提供请求的视图,该视图是使用view参数设置的。视图实现IView接口。
NotFound(name, locations) 调用此方法将创建一个ViewEngineResult对象,该对象告诉 MVC 无法找到所请求的视图。locations参数是字符串值的枚举,该字符串值描述视图引擎查找视图的位置。

在编写视图引擎时,您可以选择表21-3中描述的方法之一来指示视图请求的结果。Found方法创建一个ViewEngineResult,以指示成功的请求并为 MVC 提供要处理的视图。NotFound方法创建一个ViewEngineResult,它指示不成功的请求,并向 MVC 提供视图引擎在查找视图时搜索的位置列表(这些位置将作为错误消息的一部分显示给开发人员)。

视图引擎系统的另一个构建块是IView接口,它用于描述视图提供的功能,而不管创建视图的是哪个视图引擎。下面是IView接口:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Mvc.ViewEngines {

    public interface IView {

        string Path { get; }
        Task RenderAsync(ViewContext context);
    }
}

Path属性返回视图的路径,该路径假定视图被定义为磁盘上的文件。MVC 调用RenderAsync方法来生成对客户端的响应。Context 数据是通过ViewContext类的实例提供给视图的,它是从ActionContext派生的。除了从其父类继承的 context 属性(提供对请求、路由数据、控制器等的访问)之外,ViewContext类还提供了在渲染响应方面有用的属性,我在表21-4中描述了其中最有用的属性。

表 21-4:有用的 ViewContext 属性

名称 描述
ViewData 此属性返回一个ViewDataDictionary对象,它包含控制器提供的视图数据。
TempData 此属性返回包含临时数据的字典(如第17章所述)。
Writer 此属性返回用于从视图写入输出的TextWriter

这些属性中最有趣的是ViewData,它返回ViewDataDictionary对象。ViewDataDictionary类定义了许多有用的属性,这些属性允许访问视图模型、view bag 和视图模型元数据。我在表21-5中描述了其中最有用的属性。

表 21-5:ViewDataDictionary中有用的属性

名称 描述
Model 此对象属性返回控制器提供的模型数据
ModelMetadata 此属性返回一个ModelMetadata对象,该对象可用于反射模型数据的类型。
ModelState 此属性返回模型的状态,我在第27章中对此进行了描述。
Keys 此属性返回可用于访问ViewBag数据的键值的枚举。

查看它是如何工作的最简单的方法 —— 如何将IViewEngineViewEngineResultIView结合在一起 —— 创建一个视图引擎。我将创建一个返回一种视图的简单视图引擎。此视图将渲染一个包含有关请求和 action 方法生成的视图数据的信息的结果。这种方法可以让我演示视图引擎的工作方式,而不会陷入解析视图模板和重新创建 Razor 提供的其他特性的泥潭。

创建自定义 IView

首先,我将创建一个IView接口的实现。我在示例项目中添加了一个 Infrastructure 文件夹,并创建了一个名为 DebugDataView.cs 的新类文件,如清单21-3所示。

清单 21-3:Infrastructure 文件夹下的 DebugDataView.cs 文件的内容

using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;

namespace Views.Infrastructure
{
    public class DebugDataView : IView
    {
        public string Path => String.Empty;

        public async Task RenderAsync(ViewContext context)
        {
            context.HttpContext.Response.ContentType = "text/plain";
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("---Routing Data---");
            foreach (var kvp in context.RouteData.Values)
            {
                sb.AppendLine($"Key: {kvp.Key}, Value: {kvp.Value}");
            }
            sb.AppendLine("---View Data---");
            foreach (var kvp in context.ViewData)
            {
                sb.AppendLine($"Key: {kvp.Key}, Value: {kvp.Value}");
            }
            await context.Writer.WriteAsync(sb.ToString());
        }
    }
}

当渲染此视图时,它会将使用ViewContext参数获得的路由数据和视图数据的详细信息写入RenderAsync方法。响应是简单的文本,因此我使用 context 对象在响应的Content-Type header 上设置了 text/plain。否则,ASP.NET 默认使用 text/html,这将导致浏览器将数据显示为一行未中断的字符。

创建一个 IViewEngine 实现

视图引擎的目的是生成一个ViewEngineResult对象,该对象包含一个IView或搜索到合适视图的位置列表。现在我有了一个IView实现可以使用,可以创建视图引擎。我在 Infrastructure 文件夹中添加了一个名为 DebugDataViewEngine.cs 的类文件,其内容如清单21-4所示。

清单 21-4:Infrastructure 文件夹下的 DebugDataViewEngine.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;

namespace Views.Infrastructure
{
    public class DebugDataViewEngine : IViewEngine
    {
        public ViewEngineResult GetView(string executingFilePath, string viewPath,
            bool isMainPage)
        {
            return ViewEngineResult.NotFound(viewPath,
                new string[] { "(Debug View Engine - GetView)" });
        }
        public ViewEngineResult FindView(ActionContext context, string viewName,
            bool isMainPage)
        {
            if (viewName == "DebugData")
            {
                return ViewEngineResult.Found(viewName, new DebugDataView());
            }
            else
            {
                return ViewEngineResult.NotFound(viewName,
                    new string[] { "(Debug View Engine - FindView)" });
            }
        }
    }
}

此视图引擎中的GetView方法总是返回 NotFound 响应。FindView方法只支持一个视图,称为DebugData。当收到对具有该名称的视图的请求时,它将返回DebugDataView类的一个新实例,如下所示:

...
if (viewName == "DebugData") {
    return ViewEngineResult.Found(viewName, new DebugDataView());
}
...

如果我正在实现一个完整的视图引擎,我将利用这个机会搜索模板。实际上,这个简单的示例只需要一个DebugDataView类的新实例。如果收到对DebugData以外的视图的请求,则创建一个 NotFound 响应,如下所示:

...
return ViewEngineResult.NotFound(viewName,
    new string[] { "(Debug View Engine - FindView)" });
...

ViewEngineResult.NotFound方法假设视图引擎有查找视图所需的位置。这是一个合理的假设,因为视图通常是作为文件存储在项目中的模板文件。在这种情况下,我没有地方可以查看,所以我只返回一个虚拟位置,这将指示哪个方法被调用来定位视图。

注册自定义视图引擎

视图引擎通过配置MvcViewOptions对象在Startup类中注册,如清单21-5所示。

清单 21-5:Startup.cs 文件,注册自定义视图引擎

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
using Views.Infrastructure;

namespace Views
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.Configure<MvcViewOptions>(options => {
                options.ViewEngines.Insert(0, new DebugDataViewEngine());
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

MvcViewOptions类定义了ViewEngines属性,它是IViewEngine对象的集合。通过AddMvc方法将 Razor 添加到ViewEngine集合中,并且我用自定义类补充了默认视图引擎。

当 MVC 从一个 action 方法接收到一个ViewResult时,它会调用MvcViewOptions.ViewEngines集合中包含的每个视图引擎的FindView方法,直到它收到使用Found方法创建的ViewEngineResult为止。

如果两个或多个引擎能够为相同视图名的请求提供服务,则将引擎添加到ViewEngines.Engines集合的顺序非常重要。如果您希望视图优先,那么应该在视图引擎集合的开始处插入它,如清单21-5所示。

测试视图引擎

当应用程序启动时,浏览器将自动导航到项目的根 URL,该根 URL 将映射到 Home 控制器中的Index action。action 方法使用View方法返回指定DebugData视图的ViewResult

MVC 将转向视图引擎的集合,并开始调用它们的FindView方法。由于所请求的视图是自定义视图引擎要处理的视图,所以它为 MVC 提供了一个视图,该视图生成如图21-1所示的结果。

图21-1 使用自定义视图引擎

若要查看视图引擎无法提供视图时会发生什么,请求 /Home/List URL。这将创建一个ViewResult,并指定一个名为 List 的视图,无论是 Razor 还是自定义视图引擎都无法提供该视图。您将看到图21-2所示的错误。

图21-2 请求无法提供的视图

您可以看到,自定义视图引擎生成的消息是在搜索List视图的位置列表中报告的,旁边是 Razor 检查过的位置。

如果我想确保只使用我的视图引擎,那么我必须调用视图引擎集合上的Clear方法来删除 Razor,如清单21-6所示。

清单 21-6:Views 项目下的 Startup.cs 文件,移除其它视图引擎

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
using Views.Infrastructure;

namespace Views
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.Configure<MvcViewOptions>(options => {
                options.ViewEngines.Clear();
                options.ViewEngines.Insert(0, new DebugDataViewEngine());
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

如果再次启动应用程序并导航到 /Home/List,则只使用自定义视图引擎,如图21-3所示。

图21-3 仅在示例应用程序中使用自定义视图引擎

使用 Razor 引擎

在上一节中,我只实现了两个接口就可以创建一个自定义视图引擎。诚然,我最终得到了一些简单的东西,产生了丑陋的内容,但您看到了 MVC 是如何轻松地添加或替换核心功能的。

视图引擎中的复杂性来自视图模板系统,其中包括代码片段、支持布局和性能优化。在简单的自定义视图引擎中,我没有做任何这些事情 —— 而且没有太多的必要 —— 因为内置的 Razor 引擎提供了所有这些特性和更多功能。事实上,几乎所有 MVC 应用程序所需的功能都可用 Razor。只有一小部分项目需要解决创建自定义视图引擎的麻烦。

我在第5章中给您介绍了 Razor 语法,在本节中,我将向您展示如何使用其他特性来创建和渲染 Razor 视图。您还将学习如何自定义 Razor 引擎。

准备示例项目

为了准备利用 Razor 的示例项目,需要进行一些更改。首先,我更改了 Home 控制器的Index action,以便它选择默认视图并提供一些模型数据,如清单21-7所示。

清单 21-7:Controllers 文件夹下的 HomeController.cs 文件,更改 Index Action

using System;
using Microsoft.AspNetCore.Mvc;

namespace Views.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() =>
            View(new string[] { "Apple", "Orange", "Pear" });

        public ViewResult List() => View();
    }
}

为了给Index action 方法提供一个视图,我创建了 Views/Home 文件夹,并添加了一个名为 Index.cshtml 的视图文件,内容如清单21-8所示。

清单 21-8:Views/Home 文件夹下的 Index.cshtml 文件的内容

@model string[]
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Razor</title>
    <link asp-href-include="lib/twitter-bootstrap/css/*.min.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    This is a list of fruit names:
    @foreach (string name in Model)
    {
        <span><b>@name</b></span>
    }
</body>
</html>

视图依赖于 Bootstrap CSS库。为了将 Bootstrap 添加到示例项目中,我在 Views 项目中单击鼠标右键,在弹出菜单中选择【添加】➤【添加客户端库】,并将 twitter-bootstrap 添加至项目中。最终生成的 libman.json 配置文件代码清单18-7所示:

清单 21-9:Views 文件夹下的 libman.json 文件的内容

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    }
  ]
}

我在 Views 文件夹中创建了一个名为 _ViewImports.cshtml 的视图导入文件,其中的表达式如清单21-10所示,以启用内置标签助手。

清单 21-10:Views 文件夹下的 _ViewImports.cshtml 文件的内容

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

最后的准备步骤是重置Startup类中的视图引擎,以删除自定义引擎,并删除对禁用 Razor 的Clear方法的调用,如清单21-11所示。

清单 21-11:Startup.cs 文件,重置视图引擎

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
using Views.Infrastructure;

namespace Views
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            //services.Configure<MvcViewOptions>(options => {
            //    options.ViewEngines.Clear();
            //    options.ViewEngines.Insert(0, new DebugDataViewEngine());
            //});
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

如果运行项目,将看到图21-4所示的结果。

图21-4 运行示例应用程序

神秘莫测的 Razor 视图

了解一点 Razor 的工作原理可以帮助你理解许多功能的来历,并揭开 CSHTML 文件处理的神秘面纱。

那么,Razor 是如何将 HTML 元素和 C# 语句混合起来并生成 HTTP 响应的内容的呢?答案简单而巧妙,并建立在您在前面章节中已经了解到的 MVC 功能的基础上。Razor 将 CSHTML 文件转化为 C# 类,编译它们,然后在每次生成结果所需的视图时创建新实例。清单21-8是 Razor 为 Index.cshtml 视图创建的 C# 类:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace Asp {

    public class ASPV_Views_Home_Index_cshtml : RazorPage<string[]> {

        public IUrlHelper Url { get; private set; }

        public IViewComponentHelper Component { get; private set; }
        
        public IJsonHelper Json { get; private set; }
        
        public IHtmlHelper<string[]> Html { get; private set; }
        
        public override async Task ExecuteAsync() {
            Layout = null;
            WriteLiteral(@"<!DOCTYPE html><html><head>
                <meta name=""viewport"" content=""width=device-width"" />
                <title>Razor</title>
                <link asp-href-include=""lib/bootstrap/dist/css/*.min.css""
                    rel=""stylesheet"" />
                </head><body class=""m-1 p-1"">This is a list of fruit names:");
            foreach (string name in Model) {
                WriteLiteral("<span><b>");
                Write(name);
                WriteLiteral("</b></span>");
            }
            WriteLiteral("</body></html>");
        }
    }
}

我整理了类中的代码,以便更容易地阅读,并删除 Razor 在生成类时为检测而添加的一些 C# 语句。我将在后面的部分中对类进行细分,并解释编译后的视图是如何工作的。

注意:以前很容易查看由较早版本的 Razor 创建的类,因为每个视图都在硬盘生成一个 C# 文件,然后进行编译,以便在应用程序中使用。检查这个类只是找到正确的文件的问题。当前版本的 Razor 依赖于 C# 编译器的先进技术,允许在内存中生成和编译代码,它增强了性能,但更难看到正在发生的事情。为了获得前面显示的类,我不得不重新使用 ASP.NET Core MVC 源代码中包含的一些单元测试,这些代码为我提供了 Razor 依赖于定位和处理视图文件的类的伪实现。在日常开发中,这不是您需要做的事情,但它是一个揭示视图如何工作的过程。

理解类名

最好的起点是 Razor 创建的类的名称。

...
public class ASPV_Views_Home_Index_cshtml : RazorPage<string[]> {
...

Razor 需要一些方法来将 CSHTML 文件的名称和路径转换为它在解析文件时创建的类,并通过在类名中编码信息来实现这一点。Razor 以 ASPV 作为类名的前缀,后面是项目名称、控制器名称,最后是视图文件名;这种组合使得在 MVC 通过本章前面描述的IViewEngine请求视图时,可以很容易地检查类是否可用。

理解基础类

Razor 的许多核心特性,比如能够将视图模型引用为@Model模型,都是可能的,因为生成的类是从基类派生出来的。

...
public class ASPV_Views_Home_Index_cshtml : RazorPage<string[]> {
...

如果@model指令已用于指定模型类型,则视图类继承于RazorPage类或RazorPage<T>类。RazorPage类提供了可以在 CSHTML 文件中使用的方法和属性来访问 MVC 特性,其中最有用的方法和属性见表21-6。

表 21-6:用于视图开发的有用的 RazorPage 属性

名称 描述
Model 此属性返回 action 方法提供的模型数据
ViewData 此属性返回一个ViewDataDictionary对象,用于提供对其他视图数据特性的访问。
ViewContext 此属性返回一个ViewContext对象,如表21-4所述。
Layout 此属性用于指定布局,如第5章所述,并在本章的《使用布局 Sections》一节重新讨论。
ViewBag 此属性提供对 view bag 对象的访问,如第17章所述。
TempData 该属性提供对临时数据的访问,如第17章所述。
Context 此属性返回一个HttpContext对象,用于描述当前请求和正在准备的响应。
User 此属性返回与此请求关联的用户的配置文件。有关用户身份验证和授权的详细信息,请参阅第28章。
RenderSection() 此方法用于将视图中的一段内容插入到布局中,如本章《使用布局 Sections》一节所述。
RenderBody() 此方法将未包含在 section 中的视图中的所有内容插入布局中。有关详细信息,请参阅《使用布局 Sections》。
IsSectionDefined() 此方法用于确定视图是否定义 section

理解 Razor 页

在 ASP.NET Core 2中,Microsoft 增加了对 Razor 页面的支持,这打破了 MVC 模型,并将支持与 Razor 视图关联的文件中的视图所需的代码关联起来。这类似于 ASP.NET Web Forms 结构,它是 Microsoft 定期返回的一种设计方法,以尝试恢复旧的 Web 页面平台的简单性,而不存在第1章中描述的缺陷。

不要将本节中描述的RazorPage基类与 Razor 页面特性混淆。尽管它们的名称相似,但RazorPage基类为 MVC 框架所使用的 Razor 视图引擎提供了基础。我没有描述这本书中的 Razor 页面特性,因为它不符合 MVC 模型,也不是 MVC 平台的一部分。


Razor 还提供了一些助手属性,可以在视图中使用这些属性来生成内容,如表21-7所述。

表 21-7:Razor 助手属性

名称 描述
HtmlEncoder 此属性返回一个HtmlEncoder对象,用于在视图中安全地编码 HTML 内容
Component 此属性返回一个视图组件助手,如第22章所述
Json 该属性返回一个 JSON 助手,如《将 JSON 内容添加到视图中》一节中描述的那样
Url 该属性返回一个 URL 助手,用于使用路由配置生成 URL,如第16章所述。
Html 此属性返回一个 HTML 助手,可用于生成动态内容。这个特性在很大程度上被标签助手所取代,但仍然用于分部视图,如本章的《使用部分视图》一节所述。

表21-6和表21-7中描述的属性是您将在日常 MVC 开发中用于访问模型数据、配置视图和执行其他重要任务的属性。这些属性消除了使用 Razor 的神秘之处,并把它牢牢地放回了人们所熟知的 C# 的世界中。例如,当您使用@Model指令访问视图模型对象或使用@TempData检索临时数据值时,您引用的是由RazorPage类定义的属性。

理解视图渲染

除了向开发人员提供功能的属性和方法之外,RazorPage类还负责通过其ExecuteAsyc方法生成响应内容。此方法显示了 Razor 如何将 Index.cshtml 文件处理成一组 C# 代码:

public override async Task ExecuteAsync() {
    Layout = null;
    WriteLiteral(@"<!DOCTYPE html><html><head>
        <meta name=""viewport"" content=""width=device-width"" />
        <title>Razor</title>
        <link asp-href-include=""lib/bootstrap/dist/css/*.min.css""
            rel=""stylesheet"" />
        </head><body class=""m-1 p-1"">This is a list of fruit names:");
    foreach (string name in Model) {
        WriteLiteral("<span><b>");
        Write(name);
        WriteLiteral("</b></span>");
    }
    WriteLiteral("</body></html>");
}

数据值(例如来自Model属性的值)使用Write方法发送到客户端,后者转义字符串,使它们不会被浏览器解释为 HTML 元素。这一点很重要,因为它防止恶意数据值将内容添加到应用程序的输出中。

WriteLiteral方法不转义字符串,用于 Index.cshtml 文件中的静态内容,当然,浏览器应该将其解释为 HTML 元素。结果是,CSHTML 文件的静态和动态内容包含在一个常规的 C# 类中,并通过简单的方法调用发出。

向 Razor 视图添加动态内容

视图的全部目的是允许您将域模型部分呈现给用户。要做到这一点,您需要能够向视图添加动态内容。动态内容是在运行时生成的,可以针对每个请求进行不同的处理。这与您在编写应用程序时创建的静态内容(如 HTML)相反,并且对于每个请求都是相同的。您可以表21-8中描述的不同方式向视图添加动态内容。

表 21-8:向视图添加动态内容

技术 何时使用
内联代码 用于小的、自包含的视图逻辑片段,例如ifforeach语句.这是在视图中创建动态内容的基本工具,其他一些方法也建立在此基础上。我在第五章中介绍了这个技巧,从那以后,你已经在各章中看到了无数的例子。
标签助手 用于在 HTML 元素上生成属性。我在第23、24和25章中描述了标签助手。
Sections 用于创建将插入到布局中的特定位置的内容的 sections,如本节后面所述。
分部视图 用于在视图之间共享视图标记的子 sections。分部视图可以包含内联代码、HTML 助手方法和对其他分部视图的引用。分部视图不调用 action 方法,因此不能用于执行业务逻辑。分部视图将在本节后面描述。
视图组件 用于创建需要包含业务逻辑的可重用 UI 控件或小部件。我在第22章中描述了视图组件。

使用布局 Sections

Razor 视图引擎支持 sections 的概念,允许您在布局中提供内容区域。Razor sections 给予更大的控制权,以允许视图的哪些部分被插入到布局和他们的位置。为了演示 sections 特性,我编辑了 /Views/Home/Index.cshtml 文件,如清单21-12所示。

清单 21-12:Views/Home 文件夹下的 Index.cshtml 文件,定义 Sections

@model string[]
@{ Layout = "_Layout"; }

@section Header {
    <div class="bg-success">
        @foreach (string str in new[] { "Home", "List", "Edit" })
        {
            <a class="btn btn-sm btn-primary" asp-action="str">@str</a>
        }
    </div>
}

This is a list of fruit names:
@foreach (string name in Model)
{
    <span><b>@name</b></span>
}

@section Footer {
    <div class="bg-success">
        This is the footer
    </div>
}

我从视图中删除了一些 HTML 元素,并设置了Layout属性,以指定应该使用名为 _Layout.cshtml 的布局文件来渲染内容。

我还在视图中添加了一些 sections。Sections 是使用 Razor @section表达式定义的,后面跟着节的名称。我创建了名为 HeaderFooter 的 sections。section 的内容包含通常的 HTML 标记和 Razor 表达式的混合,在其他示例中您已经看到了这些内容。

Sections 在视图中定义,但应用于具有@RenderSection表达式的布局中。为演示这是如何工作的,我创建了 Views/Shared 文件夹,并添加了一个名为 _Layout.cshtml 的布局文件,内容如清单21-13所示。

清单 21-13:Views/Shared 文件夹下的 _Layout.cshtml 文件的内容

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link asp-href-include="lib/twitter-bootstrap/css/*.min.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    @RenderSection("Header")

    <div class="bg-info">
        This is part of the layout
    </div>

    @RenderBody()

    <div class="bg-info">
        This is part of the layout
    </div>

    @RenderSection("Footer")

    <div class="bg-info">
        This is part of the layout
    </div>
</body>
</html>

当 Razor 解析布局时,RenderSection助手方法将被替换为视图中具有指定名称的 section 的内容。视图中未包含在 section 中的部分将使用RenderBody助手插入到布局中。

通过启动应用程序可以看到这些 sections 的效果,如图21-5所示。我使用了一些 Bootstrap 样式来帮助明确输出的哪些部分来自视图,哪些部分来自布局。这个结果并不好看,但它清楚地演示了如何将视图中的内容区域放入布局中的特定位置。

图21-5 使用视图中的 section 在布局中定位内容

注意:视图只能定义布局中引用的 sections。如果您试图在视图中定义布局中没有对应@RenderSection表达式的部分,MVC 将抛出一个异常。

将这些 sections 与视图的其余部分混在一起是不寻常的。约定是在视图的开头或结尾定义 sections,以便更容易地看到哪些内容区域将被视为 sections,哪些区域将被RenderBody助手捕获。另一种方法是仅使用 sections 来定义视图,包括一个用于 body 的部分,如清单21-14所示。

清单 21-14:Views/Home 文件夹下的 Index.cshtml 文件,使用 Razor Sections 定义视图

@model string[]
@{ Layout = "_Layout"; }

@section Header {
    <div class="bg-success">
        @foreach (string str in new[] { "Home", "List", "Edit" })
        {
            <a class="btn btn-sm btn-primary" asp-action="str">@str</a>
        }
    </div>
}

@section Body {
    This is a list of fruit names:
    @foreach (string name in Model)
    {
        <span><b>@name</b></span>
    }
}

@section Footer {
    <div class="bg-success">
        This is the footer
    </div>
}

我发现这使得视图更清晰,并且减少了被RenderBody捕获的无关内容的可能性。要使用这种方法,我必须将对RenderBody助手的调用替换为RenderSection("Body"),如清单21-15所示。

清单 21-15:Views/Shared 文件夹下的 _Layout.cshtml 文件,将 body 作为 section 渲染

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link asp-href-include="lib/bootstrap/dist/css/*.min.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    @RenderSection("Header")

    <div class="bg-info">
        This is part of the layout
    </div>

    @RenderSection("Body")

    <div class="bg-info">
        This is part of the layout
    </div>

    @RenderSection("Footer")

    <div class="bg-info">
        This is part of the layout
    </div>
</body>
</html>

测试 Sections

您可以在布局中检查视图是否已定义了指定 section。当视图不需要或不希望提供特定内容时,这是一种为 section 提供默认内容的有用方法。我修改了 _Layout.cshtml 文件,以检查是否定义了Footer section,如清单21-16所示。

清单 21-16:Views/Shared 文件夹下的 _Layout.cshtml 文件,检查 Section 是否已定义

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link asp-href-include="lib/twitter-bootstrap/css/*.min.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    @RenderSection("Header")

    <div class="bg-info">
        This is part of the layout
    </div>

    @RenderSection("Body")

    <div class="bg-info">
        This is part of the layout
    </div>

    @if (IsSectionDefined("Footer"))
    {
        @RenderSection("Footer")
    }
    else
    {
        <h4>This is the default footer</h4>
    }

    <div class="bg-info">
        This is part of the layout
    </div>
</body>
</html>

如果要渲染的视图定义了要检查的 section,则IsSectionDefined助手将返回true。在该示例中,我使用此助手来确定在视图未定义Footer section 时是否应渲染某些默认内容。

渲染可选 Sections

默认情况下,视图必须包含布局中RenderSection调用的所有 sections。如果缺少 sections,MVC 将向用户报告异常。为了演示,我为一个名为 scripts 的 section 向 _Layout.cshtml 文件添加了一个新的RenderSection调用,如清单21-17所示。

清单 21-17:Views/Shared 文件夹下的 _Layout.cshtml 文件,渲染一个不存在的 section

当您启动应用程序并且 Razor 引擎尝试渲染布局和视图时,将看到如图21-6所示的错误。

图21-6 在缺少 section 时显示的错误

您可以使用IsSectionDefined方法来避免对视图未定义的 sections 进行RenderSection调用,但更优雅的方法是使用可选 sections,方法是将额外的false参数传递给RenderSection方法,如清单21-18所示。

清单 21-18:让 Section 可选

...
@RenderSection("scripts", false)
...

这将创建一个可选 section,如果视图定义该 section,则其内容将插入到结果中,否则将不会抛出异常。

使用分部视图

您通常需要在应用程序中的几个不同地方使用相同的 Razor 标记和 HTML 标记片段。为避免重复内容,可以使用分部视图,这些视图是单独的视图文件,里面的的标记和标记片段可以包含在其他视图中。在本节中,我将向您展示如何创建和使用分部视图,解释它们是如何工作的,并演示将视图数据传递到分部视图的可用技术。

创建分部视图

分部视图只是普通的 CSHTML 文件,正是它们的使用将它们与普通的 Razor 视图区别开来。Visual Studio 为创建预填充的分部视图提供了一些工具支持,但创建分部视图的最简单方法是使用 MVC 视图页面模板创建常规视图。为了演示,我将一个名为 MyPartial.cshtml 的文件添加到 Views/Home 文件夹中,并添加了清单21-19所示的内容。

清单 21-19:Views/Home 文件夹下的 MyPartial.cshtml 文件的内容

<div class="bg-info">
    <div>This is the message from the partial view.</div>
    <a asp-action="Index">This is a link to the Index action</a>
</div>

我想演示一下,您可以在分部视图中混合静态和动态内容,因此我定义了一个简单的消息,并添加了一个使用标签助手的锚元素。

应用分部视图

分部视图是通过从另一个视图中调用@Html.Partial表达式来使用的。为了演示,我在 Views/Home 文件夹中创建了一个名为 List.cshtml 的新文件,并添加了清单21-20所示的内容。

清单 21-20:Views/Home 文件夹下的 List.cshtml 文件的内容

@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Razor</title>
    <link asp-href-include="lib/bootstrap/dist/css/*.min.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    This is the List View
    @Html.Partial("MyPartial")
</body>
</html>

译者注:.NET 2.1 后,@Html.Partial("MyPartial")这句代码会发出警告,提示有出现死锁的可能。这里应使用@await Html.PartialAsync("MyPartial")进行替换

Partial方法是一个扩展方法,它应用于添加到 Razor 从视图文件中生成的类的Html属性。这是 HTML 助手的一个例子,它过去是 MVC 早期版本的视图中生成动态内容的方式,但在很大程度上已经被标签助手所取代。传递给Partial方法的参数是分部视图的名称,其内容被插入到发送给客户端的输出中。

提示:Razor 查找分部视图的方式与查找常规视图(在Views/<controller>Views/Shared文件夹中)相同。这意味着您可以创建特定于控制器的分部视图的专门版本,并覆盖 Shared 文件夹中同名的分部视图。

通过启动应用程序并导航到 Home/List URL,可以看到使用分部视图的效果,如图21-7所示。

图21-7 使用分部视图

使用强类型分部视图

您可以创建强类型的分部视图,并为它们提供视图模型对象,以便在渲染分部视图时使用。为了演示这个特性,我在 Views/Home 文件夹中创建了一个名为 MyStronglyTypedPartial.cshtml 的新视图文件,并添加了清单21-21所示的内容。

清单 21-21:Views/Home 文件夹下的 MyStronglyTypedPartial.cshtml 文件的内容

@model IEnumerable<string>

<div class="bg-info">
    This is the message from the partial view.
    <ul>
        @foreach (string str in Model)
        {
            <li>@str</li>
        }
    </ul>
</div>

视图模型类型是使用标准@model表达式定义的,我使用@foreach循环将视图模型对象的内容显示为 HTML 列表中的项。为了演示这个分部视图的使用,我更新了 /Views/List.cshtml 文件,如清单21-22所示。

清单 21-22:Views/Home 文件夹下的 List.cshtml 文件,使用强类型分部视图

@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Razor</title>
    <link asp-href-include="lib/twitter-bootstrap/css/*.min.css" rel="stylesheet" />
</head>
<body class="m-1 p-1">
    This is the List View
    @await Html.PartialAsync("MyStronglyTypedPartial",
        new string[] { "Apple", "Orange", "Pear" })
</body>
</html>

与前面的示例不同的是,我向提供视图模型的Partial助手方法传递了一个额外的参数。通过启动应用程序并导航到 /Home/List URL,您可以看到正在使用的强类型部分视图,如图21-8所示。

图21-8 使用强类型分部视图

将 JSON 内容加入视图

JSON 经常包含在视图中,以便为客户端 JavaScript 代码提供可用于动态生成内容的数据。为了准备这个示例,我通过编辑 libman.json 文件将 jQuery 包添加到应用程序中,如清单21-23所示。这将使浏览器将 JSON 数据作为 HTML 文档的一部分来处理变得更容易。

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    },
    {
      "library": "jquery@3.3.1",
      "destination": "wwwroot/lib/jquery/"
    }
  ]
}

清单21-24显示了对 List.cshtml 视图的添加,该视图使用 Razor 在发送到浏览器的响应中包含 JSON 数据。

清单 21-24:Views/Home 文件夹下的 List.cshtml 文件,使用 JSON 数据

@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Razor</title>
    <link asp-href-include="lib/twitter-bootstrap/css/*.min.css" rel="stylesheet" />
    <script id="jsonData" type="application/json">
        @Json.Serialize(new string[] { "Apple", "Orange", "Pear" })
    </script>
</head>
<body class="m-1 p-1">
    This is the List View
    <ul id="list"></ul>
</body>
</html>

@Json.Serialize表达式接受一个对象,并将其序列化为 JSON 格式。在清单中,我向视图添加了一个script元素用于包含 JSON 数据。当视图渲染并发送到浏览器时,它包含如下元素:

...
<script id="jsonData" type="application/json">["Apple","Orange","Pear"]</script>
...

为了使用 JSON 数据,清单21-25显示了添加 jQuery 库和一些内联 JavaScript 代码,这些代码使用 jQuery 解析 JSON 数据并动态创建一些 HTML 元素。

清单 21-25:Views/Home 文件夹下的 List.cshtml 文件,使用 JSON 数据

@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Razor</title>
    <link asp-href-include="lib/twitter-bootstrap/css/*.min.css" rel="stylesheet" />
    <script id="jsonData" type="application/json">
        @Json.Serialize(new string[] { "Apple", "Orange", "Pear" })
    </script>
    <script asp-src-include="lib/jquery/*.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            var list = $("#list");
            JSON.parse($("#jsonData").text()).forEach(function (val) {
                console.log("Val: " + val);
                list.append($("<li>").text(val));
            });
        });
    </script>
</head>
<body class="m-1 p-1">
    This is the List View
    <ul id="list"></ul>
</body>
</html>

如果运行示例应用程序并请求 /Home/List URL,您将看到如图21-9所示的内容。这并不是 JSON 数据最令人兴奋的用法,但它确实演示了如何将其包含在视图中。

图21-9 在视图中使用 JSON 数据

配置 Razor

可以使用在Microsoft.AspNetCore.Mvc.Razor命名空间中定义的RazorViewEngineOptions类来配置 Razor。该类定义了两个配置属性,如表21-9所述。

表 21-9:RazorViewEngineOptions 属性

名称 描述
FileProvider 此属性用于设置为 Razor 提供文件和目录内容的对象。该功能由Microsoft.AspNetCore.FileProviders.IFileProvider接口定义,默认实现是PhysicalFileProvider,它从磁盘读取文件。
ViewLocationExpanders 该属性用于配置视图扩展器,这些扩展器用于更改 Razor 定位视图的方式。

提示:如果您真的想深入挖掘,那么您可以通过在Microsoft.AspNetCore.Mvc.Razor命名空间中创建实现接口的类来替换内部的 Razor 组件,并将它们注册到Startup类中的服务提供者。这是大多数开发人员永远不需要做的事情,也不应该轻率地进行,但是如果您想要完全控制应用程序中的内容是如何处理的,这是一个有用的选择。从 http://github.com/aspnet 下载 Razor 源代码,以便开始。

FileProvider属性并不是许多应用程序都需要更改的属性,因为从磁盘读取视图文件正是大多数项目所需要的,而且 Razor 只使用 provider 加载视图,以便在应用程序第一次运行时对其进行编译。ViewLocationExpanders属性更有用,因为它允许应用程序将自定义逻辑应用于 Razor 定位视图的方式。

理解视图定位扩展器

Razor 使用视图定位扩展器来建立应该搜索视图的位置列表。视图定位扩展器实现IViewLocationExpander接口,定义如下:

using System.Collections.Generic;

namespace Microsoft.AspNetCore.Mvc.Razor {

    public interface IViewLocationExpander {

        void PopulateValues(ViewLocationExpanderContext context);

        IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
            IEnumerable<string> viewLocations);
    }
}

在接下来的部分中,我将解释视图定位扩展器是如何工作的,以及如何创建IViewLocationExpander接口的自定义实现。为了准备创建视图定位扩展程序,在清单21-26中,我更改了 Home 控制器的Index action 方法,以便它请求一个不存在的视图。这将显示 Razor 搜索视图的定位以及视图定位扩展器对视图的影响。

清单 21-26:Controllers 文件夹下的 HomeController.cs 文件,请求不存在的视图

using System;
using Microsoft.AspNetCore.Mvc;

namespace Views.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index() =>
            View("MyView", new string[] { "Apple", "Orange", "Pear" });

        public ViewResult List() => View();
    }
}

如果启动应用程序并请求默认的 URL,您将看到错误消息中显示的默认视图搜索位置,如下所示:

/Views/Home/MyView.cshtml
/Views/Shared/MyView.cshtml

创建一个简单的视图定位扩展器

最简单的视图定位扩展器只需更改 Razor 查找所有视图的位置集。这是通过实现ExpandViewLocations方法并返回要支持的位置列表来完成的。为了演示,我在 Infrastructure 文件夹中添加了一个名为 SimpleExpander.cs 的类文件,并创建了清单21-27所示的类。

清单 21-27:Infrastructure 文件夹下的 SimpleExpander.cs 文件的内容

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor;

namespace Views.Infrastructure
{
    public class SimpleExpander : IViewLocationExpander
    {
        public void PopulateValues(ViewLocationExpanderContext context)
        {
            // do nothing - not required
        }
        public IEnumerable<string> ExpandViewLocations(
            ViewLocationExpanderContext context,
            IEnumerable<string> viewLocations)
        {
            foreach (string location in viewLocations)
            {
                yield return location.Replace("Shared", "Common");
            }
            yield return "/Views/Legacy/{1}/{0}/View.cshtml";
        }
    }
}

当需要搜索位置列表时,Razor 调用ExpandViewLocations方法,并且它在viewLocations参数中以字符串序列的形式提供默认位置。位置表示为带有占位符的模板,用于 action 和控制器的名称。以下是在不使用路由 areas 的应用程序中默认使用的位置模板:

"/Views/{1}/{0}.cshtml"
"/Views/Shared/{0}.cshtml"

占位符{0}用于引用 action 方法的名称,{1}是控制器名称的占位符。视图定位扩展器的任务是返回应该搜索的位置集,在清单中,我使用string.Replace方法将默认位置中的Shared改为Common,以及添加我自己的位置,以使用不同的文件和文件夹结构。

应用视图定位扩展器

在清单21-28中,我通过在Startup类中配置 Razor 来设置视图定位扩展器。ViewLocationExpanders属性返回一个List<IViewLocationExpander>对象,我在该对象上调用Add方法。

清单 21-28:Startup.cs 文件,配置 Razor

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
using Views.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor;

namespace Views
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.Configure<RazorViewEngineOptions>(options => {
                options.ViewLocationExpanders.Add(new SimpleExpander());
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

如果运行此示例,错误消息将显示自定义视图定位扩展器提供给 Razor 的位置集。

/Views/Home/MyView.cshtml
/Views/Common/MyView.cshtml
/Views/Legacy/Home/MyView/View.cshtml

为请求选择指定视图

查看定位扩展器可以轻松更改所有请求的搜索位置,但也可以更改单个请求的搜索位置。在前面的示例中,我只实现了ExpandViewLocations方法,但真正的威力来自于PopulateValues方法,这是IViewLocationExpander扩张器接口中的另一种方法。

每次该 Razor 需要一个视图时,它都会调用视图定位扩展器的PopulateValues方法,为 context 数据提供ViewLocationExpanderContext对象。ViewLocationExpanderContext类定义了表21-10中所示的属性。

表 21-10:ViewLocationExpanderContext 属性

名称 描述
ActionContext 此属性返回一个ActionContext对象,描述已请求视图的 action 方法,并包含有关请求和响应的详细信息。
ViewName 此属性返回 action 方法请求的视图的名称
ControllerName 此属性返回包含 action 方法的控制器的名称
AreaName 如果已定义 areas,则此属性返回包含控制器的 areas 的名称。
IsMainPage 如果 Razor 正在寻找分部视图,则此属性返回false,否则返回true
Values 此属性返回一个IDictionary<string, string>,用于视图定位扩展器添加键/值对以惟一标识请求类别,如下文所述

PopulateValues方法的目的是通过向由 context 对象的Values属性返回的字典中添加键/值对来对请求进行分类。Razor 不关心请求是如何分类的,用于填充字典的方法完全留给视图定位扩展器。这很容易用一个例子来解释,所以我将一个名为 ColorExpander.cs 的类文件添加到 Infrastructure 文件夹中,并使用它来定义清单21-29所示的类。

清单 21-29:Infrastructure 文件夹下的 ColorExpander.cs 文件的内容

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Razor;
namespace Views.Infrastructure
{
    public class ColorExpander : IViewLocationExpander
    {
        private static Dictionary<string, string> Colors
            = new Dictionary<string, string>
        {
            ["red"] = "Red",
            ["green"] = "Green",
            ["blue"] = "Blue"
        };

        public void PopulateValues(ViewLocationExpanderContext context)
        {
            var routeValues = context.ActionContext.RouteData.Values;
            string color;

            if (routeValues.ContainsKey("id")
                && Colors.TryGetValue(routeValues["id"] as string, out color)
                && !string.IsNullOrEmpty(color))
            {
                context.Values["color"] = color;
            }
        }

        public IEnumerable<string> ExpandViewLocations(
            ViewLocationExpanderContext context,
            IEnumerable<string> viewLocations)
        {
            string color;
            context.Values.TryGetValue("color", out color);
            foreach (string location in viewLocations)
            {
                if (!string.IsNullOrEmpty(color))
                {
                    yield return location.Replace("{0}", color);
                }
                else
                {
                    yield return location;
                }
            }
        }
    }
}

PopulateValues方法使用ActionContext获取路由数据,并查找id URL 段的值。如果存在id段,且其值为redgreenblue,则视图定位扩展器将向Values字典添加color属性。这是分类过程:其id段与颜色匹配的请求被归类为color键,其值是从段值派生出来的。

接下来,Razor 调用ExpandViewLocations方法,并提供与PopulateValues方法使用的相同的 context 对象。这使得视图定位扩展器查看以前执行的分类,并生成 Razor 应该查看的位置集。在本例中,我使用string.Replace方法将{0}占位符与颜色名称交换。

提示:对于每个视图请求,Razor 都调用PopulateValues方法,但是缓存由ExpandViewLocations方法返回的搜索位置集。这意味着后续的请求(它的PopulateValues方法生成相同的分类键集))和值不需要调用ExpandViewLocations方法。

在清单21-30中,我配置了 Razor 以使用ColorExpander类。

清单 21-30:Views 文件夹下的 Startup.cs 文件,添加视图定位扩展器

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc;
using Views.Infrastructure;
using Microsoft.AspNetCore.Mvc.Razor;

namespace Views
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.Configure<RazorViewEngineOptions>(options => {
                options.ViewLocationExpanders.Add(new SimpleExpander());
                options.ViewLocationExpanders.Add(new ColorExpander());
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

通过启动应用程序并请求 Home/Index/red URL,您可以看到新的视图定位扩展器的效果,这将导致 Razor 在以下位置进行搜索:

/Views/Home/Red.cshtml
/Views/Common/Red.cshtml
/Views/Legacy/Home/Red/View.cshtml

同样,对 /Home/Index/green URL 的请求将导致 Razor 在以下位置搜索:

/Views/Home/Green.cshtml
/Views/Common/Green.cshtml
/Views/Legacy/Home/Green/View.cshtml

视图定位扩展器注册的顺序非常重要,因为由一个扩展器的ExpandViewLocations方法生成的位置集被用作列表中下一个扩展器的viewLocations参数。您可以在前面显示的位置中看到这一点,在这些位置中 Views/Common 和 Views/Legacy 位置是由SimpleExpander类生成的,该类出现在Startup类的ColorExpander之前。

总结

在本章中,我演示了如何创建自定义视图引擎,并解释了 Razor 是如何通过将 CSHTML 文件转换为 C# 进行工作的。我向您展示了如何使用布局 sections 和分部视图,并演示了如何更改 Razor 用于定位视图文件的位置。在下一章中,我将描述视图组件,这些组件用于提供支持分部视图的逻辑。

;

© 2018 - IOT小分队文章发布系统 v0.3